# Copyright 2013 Red Hat, Inc.
#
#    Licensed under the Apache License, Version 2.0 (the "License"); you may
#    not use this file except in compliance with the License. You may obtain
#    a copy of the License at
#
#         http://www.apache.org/licenses/LICENSE-2.0
#
#    Unless required by applicable law or agreed to in writing, software
#    distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
#    WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
#    License for the specific language governing permissions and limitations
#    under the License.

"""Provides Guru Meditation Report

This module defines the actual OpenStack Guru Meditation
Report class.

This can be used in the OpenStack command definition files.
For example, in a nova command module (under nova/cmd):

.. code-block:: python
   :emphasize-lines: 8,9,10

   from oslo_config import cfg
   from oslo_log import log as oslo_logging
   from oslo_reports import opts as gmr_opts
   from oslo_reports import guru_meditation_report as gmr

   CONF = cfg.CONF
   # maybe import some options here...

   def main():
       oslo_logging.register_options(CONF)
       gmr_opts.set_defaults(CONF)

       CONF(sys.argv[1:], default_config_files=['myapp.conf'])
       oslo_logging.setup(CONF, 'myapp')

       gmr.TextGuruMeditation.register_section('Some Special Section',
                                           special_section_generator)
       gmr.TextGuruMeditation.setup_autorun(version_object, conf=CONF)

       server = service.Service.create(binary='some-service',
                                       topic=CONF.some_service_topic)
       service.serve(server)
       service.wait()

Then, you can do

.. code-block:: bash

   $ kill -USR2 $SERVICE_PID

and get a Guru Meditation Report in the file or terminal
where stderr is logged for that given service.
"""

from __future__ import print_function

import inspect
import logging
import os
import signal
import stat
import sys
import threading
import time
import traceback

from oslo_utils import timeutils

from oslo_reports._i18n import _LE
from oslo_reports._i18n import _LW
from oslo_reports.generators import conf as cgen
from oslo_reports.generators import process as prgen
from oslo_reports.generators import threading as tgen
from oslo_reports.generators import version as pgen
from oslo_reports import report


LOG = logging.getLogger(__name__)


class GuruMeditation(object):
    """A Guru Meditation Report Mixin/Base Class

    This class is a base class for Guru Meditation Reports.
    It provides facilities for registering sections and
    setting up functionality to auto-run the report on
    a certain signal or use file modification events.

    This class should always be used in conjunction with
    a Report class via multiple inheritance.  It should
    always come first in the class list to ensure the
    MRO is correct.
    """

    timestamp_fmt = "%Y%m%d%H%M%S"

    def __init__(self, version_obj, sig_handler_tb=None, *args, **kwargs):
        self.version_obj = version_obj
        self.traceback = sig_handler_tb

        super(GuruMeditation, self).__init__(*args, **kwargs)
        self.start_section_index = len(self.sections)

    @classmethod
    def register_section(cls, section_title, generator):
        """Register a New Section

        This method registers a persistent section for the current
        class.

        :param str section_title: the title of the section
        :param generator: the generator for the section
        """

        try:
            cls.persistent_sections.append([section_title, generator])
        except AttributeError:
            cls.persistent_sections = [[section_title, generator]]

    @classmethod
    def setup_autorun(cls, version, service_name=None,
                      log_dir=None, signum=None, conf=None):
        """Set Up Auto-Run

        This method sets up the Guru Meditation Report to automatically
        get dumped to stderr or a file in a given dir when the given signal
        is received. It can also use file modification events instead of
        signals.

        :param version: the version object for the current product
        :param service_name: this program name used to construct logfile name
        :param logdir: path to a log directory where to create a file
        :param signum: the signal to associate with running the report
        :param conf: Configuration object, managed by the caller.
        """

        if log_dir is None and conf is not None:
            log_dir = conf.oslo_reports.log_dir

        if signum:
            cls._setup_signal(signum, version, service_name, log_dir)
            return

        if conf and conf.oslo_reports.file_event_handler:
            cls._setup_file_watcher(
                conf.oslo_reports.file_event_handler,
                conf.oslo_reports.file_event_handler_interval,
                version, service_name, log_dir)
        else:
            if hasattr(signal, 'SIGUSR1'):
                # TODO(dims) We need to remove this in the "O" release cycle
                LOG.warning(_LW("Guru meditation now registers SIGUSR1 and "
                                "SIGUSR2 by default for backward "
                                "compatibility. SIGUSR1 will no longer be "
                                "registered in a future release, so please "
                                "use SIGUSR2 to generate reports."))
                cls._setup_signal(signal.SIGUSR1,
                                  version, service_name, log_dir)
            if hasattr(signal, 'SIGUSR2'):
                cls._setup_signal(signal.SIGUSR2,
                                  version, service_name, log_dir)

    @classmethod
    def _setup_file_watcher(cls, filepath, interval, version, service_name,
                            log_dir):

        st = os.stat(filepath)
        if not bool(st.st_mode & stat.S_IRGRP):
            LOG.error(_LE("Guru Meditation Report does not have read "
                          "permissions to '%s' file."), filepath)

        def _handler():
            mtime = time.time()
            while True:
                try:
                    stat = os.stat(filepath)
                    if stat.st_mtime > mtime:
                        cls.handle_signal(version, service_name, log_dir, None)
                        mtime = stat.st_mtime
                except OSError:
                    msg = ("Guru Meditation Report cannot read " +
                           "'{0}' file".format(filepath))
                    raise IOError(msg)
                finally:
                    time.sleep(interval)

        th = threading.Thread(target=_handler)
        th.daemon = True
        th.start()

    @classmethod
    def _setup_signal(cls, signum, version, service_name, log_dir):
        signal.signal(signum,
                      lambda sn, f: cls.handle_signal(
                          version, service_name, log_dir, f))

    @classmethod
    def handle_signal(cls, version, service_name, log_dir, frame):
        """The Signal Handler

        This method (indirectly) handles receiving a registered signal and
        dumping the Guru Meditation Report to stderr or a file in a given dir.
        If service name and log dir are not None, the report will be dumped to
        a file named $service_name_gurumeditation_$current_time in the log_dir
        directory.
        This method is designed to be curried into a proper signal handler by
        currying out the version
        parameter.

        :param version: the version object for the current product
        :param service_name: this program name used to construct logfile name
        :param logdir: path to a log directory where to create a file
        :param frame: the frame object provided to the signal handler
        """

        try:
            res = cls(version, frame).run()
        except Exception:
            traceback.print_exc(file=sys.stderr)
            print("Unable to run Guru Meditation Report!",
                  file=sys.stderr)
        else:
            if log_dir:
                service_name = service_name or os.path.basename(
                    inspect.stack()[-1][1])
                filename = "%s_gurumeditation_%s" % (
                    service_name, timeutils.utcnow().strftime(
                        cls.timestamp_fmt))
                filepath = os.path.join(log_dir, filename)
                try:
                    with open(filepath, "w") as dumpfile:
                        dumpfile.write(res)
                except Exception:
                    print("Unable to dump Guru Meditation Report to file %s" %
                          (filepath,), file=sys.stderr)
            else:
                print(res, file=sys.stderr)

    def _readd_sections(self):
        del self.sections[self.start_section_index:]

        self.add_section('Package',
                         pgen.PackageReportGenerator(self.version_obj))

        self.add_section('Threads',
                         tgen.ThreadReportGenerator(self.traceback))

        self.add_section('Green Threads',
                         tgen.GreenThreadReportGenerator())

        self.add_section('Processes',
                         prgen.ProcessReportGenerator())

        self.add_section('Configuration',
                         cgen.ConfigReportGenerator())

        try:
            for section_title, generator in self.persistent_sections:
                self.add_section(section_title, generator)
        except AttributeError:
            pass

    def run(self):
        self._readd_sections()
        return super(GuruMeditation, self).run()


# GuruMeditation must come first to get the correct MRO
class TextGuruMeditation(GuruMeditation, report.TextReport):
    """A Text Guru Meditation Report

    This report is the basic human-readable Guru Meditation Report

    It contains the following sections by default
    (in addition to any registered persistent sections):

    - Package Information

    - Threads List

    - Green Threads List

    - Process List

    - Configuration Options

    :param version_obj: the version object for the current product
    :param traceback: an (optional) frame object providing the actual
                      traceback for the current thread
    """

    def __init__(self, version_obj, traceback=None):
        super(TextGuruMeditation, self).__init__(version_obj, traceback,
                                                 'Guru Meditation')
